fix(reports): make the generated CSV actually downloadable - #11
Merged
Conversation
generate_report returned `/api/reports/<id>/download`, which the model wrote into chat as a link. That route needs the session bearer header and a browser navigation carries none, so every click 401'd. - generate_report returns a job id and a bounded preview, never the URL - the tool call card gets a Download button that fetches with the token and saves the blob - new download_report tool for agents with no UI: execute() runs in the page's session, so the page downloads on their behalf - the download route sends Content-Disposition
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
generate_reportfinished and handed the user a dead link.reports.service.tssetjob.url = /api/reports/<id>/download, the tool returned it, and the model wrote it into chat as markdown. Nothing about that could work:Content-Disposition, so even authenticated the CSV would render inline instead of saving.Verified against the running backend:
200with the bearer,401without.The fix
Replace the link with a real control, and stop handing the model a URL it cannot make work.
generate_reportreturns{ jobId, rows, filename, preview, previewTruncated }— never the URL. The preview is bounded (header + 20 rows) and says when it is truncated.ApiClient.download()with the token, saves via a blob (core/download/save-file.ts,core/reports/report-download.ts).download_report— a new WebMCP tool, so this also works for a client that can only call tools.execute()runs inside the page in the authenticated session, so the page performs the fetch and the browser save on the agent's behalf. Requires thejobIdfromgenerate_report; annotatedreadOnlyHint: false(a file lands on the user's machine) with no confirmation (asking to download is the confirmation).Content-Disposition: attachmentwith a range-named file,actuo-expenses-<from>_<to>.csv.ReportDownloadis split deliberately:save()throws and has no in-flight guard (the tool — a tool that reports a save that never happened lies to the model),download()keeps the guard and the per-job error signal (the button).Tests
841 frontend + 161 backend unit + 37 e2e, all green. New coverage for the controller (which had none),
saveBlob,ReportDownload, and the card's download affordance. Three existing exact-list guards caught the new tool and were updated.Not verified here
The Copilot chat path needs a Gemini key in the browser (BYOK), so the model actually choosing to call
download_reportis worth one manual pass. ThegetTools()check for a third-party client also needs a signed-in session.